﻿using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using System.Collections;
using System.Collections.Generic;
using System;
using System.Threading;
using System.Diagnostics;

namespace block_explorer
{
    class MapSector : IMeshableCube
    {
        // how many blocks in each axis
        public const int SIZE = 16;

        Map mMap;
        SectorAddress mAddress;

        BoundingBox mBounds;
        Vector3 mPosition;

        Block[] mBlocks;
        VertexBuffer mVBuffer;

        IDictionary<Vector3, int> mLightSources;

        bool mSky;
        bool mVisible;
        bool mBuilt;
        bool mLit;
        bool mNeedsMeshRefresh;
        bool mNeedsLightsRefresh;
        bool mNeedsFullLightsRefresh;

        // a list of sector addresses we're waiting for so we can fill the mesh
        private List<SectorAddress> mMeshDependancies;
        private List<SectorAddress> mLightDependancies;

        int mVertCount;
        int mTriCount;

        #region Constructors
        public MapSector(Map map)
        {
            mMap = map;
            mBuilt = false;
            mLit = false;
            mMeshDependancies = new List<SectorAddress>();
            mLightDependancies = new List<SectorAddress>();
            mVertCount = 0;
            mTriCount = 0;
            mLightSources = new Dictionary<Vector3, int>();
        }
        #endregion

        #region Properties
        public SectorAddress Address
        {
            get { return mAddress; }
            set
            {
                mAddress = value;
                mPosition = mAddress.ToWorldPosition();
                mBounds = new BoundingBox(Position, Position + new Vector3(Block.SIZE * SIZE, Block.SIZE * SIZE, Block.SIZE * SIZE));
            }
        }
        public Vector3 Position { get { return mPosition; } }
        public BoundingBox Bounds { get { return mBounds; } }
        public bool SkySector { get { return mSky; } set { mSky = value; } }
        public bool Visible { get { return mVisible; } set { mVisible = value; } }
        public bool Built { get { return mBuilt; } }
        public bool Lit { get { return mLit; } }
        public bool NeedsMeshRefresh { get { return mNeedsMeshRefresh; } set { mNeedsMeshRefresh = value; } }
        public bool NeedsLightsRefresh { get { return /*mNeedsLightsRefresh*/false; } set { mNeedsLightsRefresh = value; } }
        public bool NeedsFullLightsRefresh { get { return /*mNeedsFullLightsRefresh*/false; } set { mNeedsFullLightsRefresh = value; } }
        public int VertCount { get { return mVertCount; } }
        public int TriCount { get { return mTriCount; } }
        #endregion

        #region Block Information Access
        /**
         * Returns true if this sector contains the given local position
         */
        public bool Contains(Vector3 localPosition)
        {
            return localPosition.X >= 0 && localPosition.X < SIZE
                && localPosition.Y >= 0 && localPosition.Y < SIZE
                && localPosition.Z >= 0 && localPosition.Z < SIZE;
        }

        private SectorAddress GetNeighborFromCell(Vector3 cell)
        {
            int secDirX = cell.X < 0 ? -1 : cell.X >= SIZE ? 1 : 0;
            int secDirY = cell.Y < 0 ? -1 : cell.Y >= SIZE ? 1 : 0;
            int secDirZ = cell.Z < 0 ? -1 : cell.Z >= SIZE ? 1 : 0;
            return Address + new SectorAddress(secDirX, secDirY, secDirZ);
        }

        public void ModifyBlockAt(Vector3 localPosition, Block.ContentType contentType)
        {
            Block b = GetBlockAt(localPosition);
            b.Content = contentType;
            NeedsMeshRefresh = true;

            // refresh neighbors if the block lies on a boundary
            SectorAddress neighborAddress;
            if (localPosition.X <= 0)
            {
                neighborAddress = Address + new SectorAddress(-1, 0, 0);
                mMap.GetSector(neighborAddress).NeedsMeshRefresh = true;
            }
            else if (localPosition.X >= (SIZE - 1))
            {
                neighborAddress = Address + new SectorAddress(1, 0, 0);
                mMap.GetSector(neighborAddress).NeedsMeshRefresh = true;
            }

            if (localPosition.Y <= 0)
            {
                neighborAddress = Address + new SectorAddress(0, -1, 0);
                mMap.GetSector(neighborAddress).NeedsMeshRefresh = true;
            }
            else if (localPosition.Y >= (SIZE - 1))
            {
                neighborAddress = Address + new SectorAddress(0, 1, 0);
                mMap.GetSector(neighborAddress).NeedsMeshRefresh = true;
            }

            if (localPosition.Z <= 0)
            {
                neighborAddress = Address + new SectorAddress(0, 0, -1);
                mMap.GetSector(neighborAddress).NeedsMeshRefresh = true;
            }
            else if (localPosition.Z >= (SIZE - 1))
            {
                neighborAddress = Address + new SectorAddress(0, 0, 1);
                mMap.GetSector(neighborAddress).NeedsMeshRefresh = true;
            }
        }

        #endregion

        #region IMeshableCube interface

        public int Size { get { return SIZE; } }

        public float BlockSize { get { return Block.SIZE; } }

        public bool IsSolidBlockAt(Vector3 localPosition)
        {
            Debug.Assert(Contains(localPosition), "localPosition must be a local coord");
            int index = (int)((localPosition.X * SIZE * SIZE) + (localPosition.Y * SIZE) + localPosition.Z);
            return GetBlockAt(localPosition).IsSolid();
        }

        public Block GetBlockAt(Vector3 localPosition)
        {
            Block block;

            if (Contains(localPosition))
            {
                // neighbor is inside the sector
                if (mBlocks == null) return null;
                int index = (int)((Math.Floor(localPosition.X) * SIZE * SIZE) + (Math.Floor(localPosition.Y) * SIZE) + Math.Floor(localPosition.Z));
                block = (Block)mBlocks[index];
            }
            else
            {
                // neighbor crosses sector boundary. might not exist yet
                Vector3 neighborPosition = Position + localPosition;
                block = mMap.GetBlockAt(neighborPosition);
                if (block == null)
                {
                    // sector hasnt been generated yet
                    // add it to the waiting for list so we can refresh it later
                    SectorAddress address = GetNeighborFromCell(localPosition);
                    RegisterSectorMeshDependancy(address);
                }
            }
            return block;
        }

        #endregion

        #region Building methods
        public void CacheBlocks()
        {
            PerformanceTimer timer = new PerformanceTimer();

            mBlocks = new Block[SIZE * SIZE * SIZE];

            // build up the block list
            for (int x = 0; x < SIZE; ++x)
            {
                for (int y = 0; y < SIZE; ++y)
                {
                    for (int z = 0; z < SIZE; ++z)
                    {
                        Vector3 localPosition = new Vector3(x, y, z);
                        mBlocks[(x * SIZE * SIZE) + (y * SIZE) + z] = mMap.Builder.GetBlock(Position + localPosition);
                    }
                }
                Thread.Sleep(0);
            }

            mBuilt = true;
        }

        public void RefreshMesh(GraphicsDevice device)
        {
            PerformanceTimer timer = new PerformanceTimer();

            CubeMesher cm = new CubeMesher(this, device);
            mVBuffer = cm.GetVertexBuffer();
            mTriCount = cm.TriCount;


            mNeedsMeshRefresh = false;
        }

        #endregion

        #region Lighting Methods
        public void AddLightSource(Vector3 cell, int lightValue)
        {
            lock (mLightSources)
            {
                if (!mLightSources.ContainsKey(cell))
                {
                    mLightSources[cell] = lightValue;
                    mNeedsLightsRefresh = true;
                }
            }
        }

        public void RemoveLightSource(Vector3 cell)
        {
            lock (mLightSources)
            {
                if (!mLightSources.ContainsKey(cell))
                {
                    return;
                }

                // work out all sectors that are affected by this light source
                int lightPower = mLightSources[cell];
                mLightSources.Remove(cell);
                List<SectorAddress> affected = new List<SectorAddress>();
                if (cell.X < lightPower)
                    affected.Add(Address + new SectorAddress(-1, 0, 0));
                else if (cell.X + lightPower > SIZE)
                    affected.Add(Address + new SectorAddress(1, 0, 0));
                if (cell.Y < lightPower)
                    affected.Add(Address + new SectorAddress(0, -1, 0));
                else if (cell.Y + lightPower > SIZE)
                    affected.Add(Address + new SectorAddress(0, 1, 0));
                if (cell.Z < lightPower)
                    affected.Add(Address + new SectorAddress(0, 0, -1));
                else if (cell.Z + lightPower > SIZE)
                    affected.Add(Address + new SectorAddress(0, 0, 1));
                // need diagonals

                foreach (SectorAddress address in affected)
                {
                    MapSector sector = mMap.GetSector(address);
                    if (sector != null)
                    {
                        sector.NeedsFullLightsRefresh = true;
                    }
                }
                NeedsFullLightsRefresh = true;
            }
        }

        public void ClearLightData()
        {
            mNeedsFullLightsRefresh = false;

            for (int x = 0; x < SIZE; ++x)
            {
                for (int y = 0; y < SIZE; ++y)
                {
                    for (int z = 0; z < SIZE; ++z)
                    {
                        GetBlockAt(new Vector3(x, y, z)).LightValue = 0;
                    }
                }
            }
        }

        public void RefreshLights()
        {
            PerformanceTimer timer = new PerformanceTimer();

            mNeedsLightsRefresh = false;

            // TODO
            Sunlight();

            // copy the list of lights so we dont get thread issues
            IDictionary<Vector3, int> lightSources;
            lock (mLightSources)
            {
                lightSources = new Dictionary<Vector3, int>(mLightSources);
            }

            foreach (var kvp in lightSources)
            {
                LightFromCell(kvp.Key, kvp.Value);
            }

            mLit = true;
        }

        private void LightFromCell(Vector3 sourceCell, int lightValue)
        {
            List<Vector3> openList = new List<Vector3>();
            HashSet<Vector3> closedList = new HashSet<Vector3>();

            // first light the source cell
            Block block = GetBlockAt(sourceCell);
            Debug.Assert(block != null, "Light source outside sector");
            if (block.IsSolid())
                return;
            block.LightValue = lightValue;

            // all cell coords from here on should be world space
            sourceCell += Position;
            openList.Add(sourceCell);

            do
            {
                Vector3 cell = (Vector3)openList[0]; // cell in world space
                closedList.Add(cell);
                openList.Remove(cell);

                // grab a source cell and light its neighbours
                // all source cells should exist
                block = mMap.GetBlockAt(cell);
                Debug.Assert(block != null, "All source cells should exist");
                Debug.Assert(!block.IsSolid(), "Source cells shouldnt be solid");

                // reduce the spread light by a set amount
                int light = block.LightValue - 1;
                if (light <= 0) continue; // if we've got no light to spread we're done

                // for all neighbours in cardinal directions
                foreach (Vector3 neighbourCell in new Util.CellCardinalIterator(cell))
                {
                    LightVisitCell(sourceCell, neighbourCell, light, ref openList, ref closedList);
                }

            } while (openList.Count > 0);
        }

        private void LightVisitCell(Vector3 source, Vector3 nextCell, int lightValue, ref List<Vector3> openList, ref HashSet<Vector3> closedList)
        {
            Debug.Assert(lightValue > 0, "Trying to light with zero value");

            Block nextBlock = mMap.GetBlockAt(nextCell);

            if (nextBlock == null)
            {
                // the sector containing this block has not yet been created
                // trigger a refresh once that sector is available
                RegisterSectorLightDependancy(SectorAddress.FromWorldPosition(nextCell));
                return;
            }
            
            // make sure the cell we're lighting hasnt already been lit and is not solid
            bool alreadyVisited = closedList.Contains(nextCell);
            bool solidBlock = nextBlock.IsSolid();
            if (!alreadyVisited && !solidBlock)
            {
                // only apply the light value if its not already brighter
                if (nextBlock.LightValue < lightValue)
                {
                    // line of sight check with the source
                    //Vector3 wSource = Block.GetCenter(source);
                    //Vector3 wDest = Block.GetCenter(nextCell);
                    //Vector3 cell, t;
                    //if (!mMap.IntersectLineSegment(wSource, wDest, out cell, out t))
                    {
                        nextBlock.LightValue = lightValue;
                        openList.Add(nextCell);
                    }
                }
                SectorAddress ownerAddress = SectorAddress.FromWorldPosition(nextCell);
                if (ownerAddress != Address)
                {
                    mMap.GetSector(ownerAddress).NeedsMeshRefresh = true;
                }
            }
        }

        public void Sunlight()
        {
            // propagate sky blocks down through columns

            MapSector sector = mMap.GetSector(Address + new SectorAddress(0, 1, 0));
            if (mSky || (sector != null && sector.Lit))
            {
                for (int x = 0; x < SIZE; ++x)
                {
                    for (int z = 0; z < SIZE; ++z)
                    {
                        Block topBlock = mSky ? null : sector.GetBlockAt(new Vector3(x, 0, z));
                        if (mSky || topBlock.Sky)
                        {
                            for (int y = SIZE - 1; y >= 0; --y)
                            {
                                Block b = GetBlockAt(new Vector3(x, y, z));
                                if (b.IsSolid())
                                {
                                    if (y < SIZE - 1)
                                    {
                                        AddLightSource(new Vector3(x, y+1, z), 15);
                                    }
                                    break;
                                }
                                b.Sky = true;
                                b.LightValue = 15;

                                if (y == 0)
                                {
                                    MapSector below = mMap.GetSector(Address + new SectorAddress(0, -1, 0));
                                    if (below != null)
                                    {
                                        below.mNeedsLightsRefresh = true;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        #endregion

        #region Dependency registration methods
        // registers a sector as a dependancy to building the mesh for this
        // sector. when the sector becomes 
        private void RegisterSectorMeshDependancy(SectorAddress address)
        {
            lock (mMeshDependancies)
            {
                if (mMeshDependancies.IndexOf(address) < 0)
                {
                    mMeshDependancies.Add(address);
                }
            }
        }

        private void RegisterSectorLightDependancy(SectorAddress address)
        {
            lock (mLightDependancies)
            {
                if (mLightDependancies.IndexOf(address) < 0)
                {
                    mLightDependancies.Add(address);
                }
            }
        }
        #endregion

        #region Update methods
        public void Update(GameTime gameTime)
        {
            lock (mMeshDependancies)
            {
                foreach (SectorAddress address in mMeshDependancies)
                {
                    MapSector sector = mMap.GetSector(address);
                    if (sector != null && sector.Built)
                    {
                        mNeedsMeshRefresh = true;
                        mMeshDependancies.Clear();
                        break;
                    }
                }
            }

            lock (mLightDependancies)
            {
                foreach (SectorAddress address in mLightDependancies)
                {
                    MapSector sector = mMap.GetSector(address);
                    if (sector != null && sector.Lit)
                    {
                        mNeedsLightsRefresh = true;
                        mLightDependancies.Clear();
                        break;
                    }
                }
            }
        }
        #endregion

        #region Draw Methods
        public void Draw(GraphicsDevice device)
        {
            if (!mBuilt)
                return;

            if (mTriCount <= 0)
                return;

            //if (!mLit)
            //    return;

            lock (this)
            {
                device.SetVertexBuffer(mVBuffer);
                device.DrawPrimitives(PrimitiveType.TriangleList, 0, mTriCount);
            }
        }

        public void Draw2D(GraphicsDevice device)
        {
            if (!mBuilt)
                return;

            if (mTriCount <= 0)
                return;

            if (mMap.SectorBoundaryVertexbuffer == null)
                return;

            lock (this)
            {
                device.SetVertexBuffer(mMap.SectorBoundaryVertexbuffer);
                device.DrawPrimitives(PrimitiveType.LineList, 0, 12);
            }
        }

        #endregion

        #region Static helpers
        public static BoundingBox BoundsFromAddress(SectorAddress address)
        {
            Vector3 min = new Vector3(address.X * SIZE, address.Y * SIZE, address.Z * SIZE);
            return new BoundingBox(min, min + new Vector3(SIZE, SIZE, SIZE));
        }
        #endregion
    }
}
